# -*- coding: utf-8 -*-
# @Time    : 2024/7/12 13:40
# @Author  : yujiahao
# @File    : 21_fastapi_excrption.py
# @description:处理错误

"""
在某些情况下，需要向客户端返回错误提示。这里所谓的客户端包括前端浏览器、其他应用程序、物联网设备等。

需要向客户端返回错误提示的场景主要如下：

1. 客户端没有执行操作的权限：
   - 403 Forbidden: 客户端已认证但没有执行该操作的权限。
     示例：用户尝试删除不属于自己的资源。

2. 客户端没有访问资源的权限：
   - 401 Unauthorized: 客户端需要认证才能访问资源。
     示例：用户尝试访问需要登录才能查看的页面，但未提供有效的认证信息。
   - 403 Forbidden: 客户端已认证但没有访问该资源的权限。
     示例：用户尝试访问不属于自己的资源。

3. 客户端要访问的项目不存在：
   - 404 Not Found: 服务器无法找到客户端请求的资源。
     示例：用户尝试访问一个不存在的网页或资源。

4. 请求格式错误或参数不正确：
   - 400 Bad Request: 服务器无法处理请求，因为请求格式错误或参数不正确。
     示例：客户端发送了一个缺少必需参数的表单。

5. 请求方法不被允许：
   - 405 Method Not Allowed: 请求的方法（如 GET、POST）不被允许。
     示例：用户尝试使用 POST 方法访问只允许 GET 方法的资源。

6. 请求超出速率限制：
   - 429 Too Many Requests: 客户端发送了太多请求，超过了服务器的速率限制。
     示例：用户在短时间内频繁刷新页面或发送请求。

遇到这些情况时，通常要返回 4XX（400 至 499）HTTP 状态码。4XX 状态码与表示请求成功的 2XX（200 至 299） HTTP 状态码类似，只不过，4XX 状态码表示客户端发生的错误。

以下是一些常见的 4XX 状态码及其含义：
- 400 Bad Request: 请求格式错误或参数不正确。
- 401 Unauthorized: 需要认证才能访问资源。
- 403 Forbidden: 已认证但没有权限。
- 404 Not Found: 请求的资源不存在。
- 405 Method Not Allowed: 请求方法不被允许。
- 429 Too Many Requests: 请求超出速率限制。

这些状态码帮助客户端理解请求失败的原因，并采取适当的措施进行修正。
"""

# 使用HTTPException，向客户端返回错误响应
from fastapi import FastAPI, HTTPException, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from starlette import status
from starlette.responses import JSONResponse, PlainTextResponse

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    '''
    触发 HTTPException

    HTTPException 是额外包含了和 API 有关数据的常规 Python 异常。

    因为是 Python 异常，所以不能 return，只能 raise。

    如在调用路径操作函数里的工具函数时，触发了 HTTPException，FastAPI 就不再继续执行路径操作函数中的后续代码，而是立即终止请求，
    并把 HTTPException 的 HTTP 错误发送至客户端。

    - 触发 HTTPException 时，可以用参数 detail 传递任何能转换为 JSON 的值，不仅限于 str。
    - 还支持传递 dict、list 等数据结构。
    - FastAPI 能自动处理这些数据，并将之转换为 JSON。
    '''

    if item_id not in items:
        # 可以直接用404，也可以直接用这个方法
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
    return {"item": items[item_id]}


# todo 添加自定义响应头
'''

有些场景下要为 HTTP 错误添加自定义响应头。例如，出于某些方面的安全需要。

一般情况下可能不会需要在代码中直接使用响应头。

但对于某些高级应用场景，还是需要添加自定义响应头：
'''


@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item": items[item_id]}


# todo 安装自定义异常处理器

'''
添加自定义处理器，要使用 Starlette 的异常工具。(fastapi.tiangolo.com/zh/tutorial/handling-errors/)

假设要触发的自定义异常叫作 UnicornException。

且需要 FastAPI 实现全局处理该异常。

此时，可以用 @app.exception_handler() 添加自定义异常控制器：


自定义异常处理流程总结：

1. 定义自定义异常类
   - 创建一个继承自 Python 内置 Exception 类的自定义异常类。
   - 可以在自定义异常类中添加额外的属性和方法，以便更好地描述和处理特定的错误情景。

2. 创建 FastAPI 应用实例
   - 使用 FastAPI() 创建一个应用实例，这是所有路径操作和异常处理器注册的地方。

3. 定义并注册异常处理器
   - 定义一个函数来处理自定义异常，并使用 @app.exception_handler 装饰器将其注册到 FastAPI 应用实例中。
   - 处理器函数需要接受两个参数：Request 对象和自定义异常实例。
   - 处理器函数应该返回一个响应对象，例如 JSONResponse。

4. 在路径操作中引发自定义异常
   - 在路径操作函数中，根据需要引发自定义异常。这可以是基于某些业务逻辑或条件的判断。

5. 运行 FastAPI 应用
   - 确保你的 FastAPI 应用正在运行。可以使用 uvicorn 来运行应用。

6. 处理和响应
   - 当客户端请求触发路径操作中的自定义异常时，FastAPI 会捕获该异常并调用相应的异常处理器。
   - 处理器返回一个自定义响应，通常是一个 JSON 响应，包含错误的详细信息和适当的 HTTP 状态码。

通过这些步骤，可以在 FastAPI 中实现自定义异常处理，提供更灵活和可读的错误响应。


技术细节：
    
from starlette.requests import Request 和 from starlette.responses import JSONResponse 
也可以用于导入 Request 和 JSONResponse。

FastAPI 提供了与 starlette.responses 相同的 fastapi.responses 作为快捷方式，
但大部分响应操作都可以直接从 Starlette 导入。同理，Request 也是如此。
'''


# 自定义一个异常类继承Exception
class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


# 定义自定义异常处理器,并使用 @app.exception_handler 装饰器注册
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
    )


@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    #  在路径操作中引发自定义异常
    if name == "yujiahao":
        raise UnicornException(name=name)
    return {"unicorn_name": name}


# todo 覆盖默认异常处理器
"""
覆盖 FastAPI 默认的请求验证异常处理器的步骤：

1. FastAPI 默认异常处理器
   - FastAPI 自带了一些默认异常处理器，如处理 HTTPException 和请求无效数据时会返回默认的 JSON 响应。

2. 自定义异常处理器
   - 可以使用自定义处理器来覆盖这些默认处理器。

3. RequestValidationError
   - 当请求中包含无效数据时，FastAPI 会触发 RequestValidationError 异常。
   - 这个异常有一个内置的默认处理器。

4. 覆盖默认处理器
   - 要覆盖这个默认处理器，可以导入 RequestValidationError 并用 @app.exception_handler(RequestValidationError) 装饰自定义异常处理器。
   - 这样，处理器就可以接收 Request 对象和异常实例，从而处理请求验证异常。
"""


# 自定义 HTTPException 异常处理器
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    """
    处理 HTTPException 异常。
    当 HTTPException 被触发时，返回一个纯文本响应，包含异常的详细信息和状态码。
    """
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


# 自定义 RequestValidationError 异常处理器
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    """
    处理 RequestValidationError 异常。
    RequestValidationError 包含其接收到的无效数据请求的 body 。
    可以用这个请求体生成日志、调试错误，并返回给用户
    """
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )


# 定义一个路径操作，在这里进行异常的抛出
@app.get("/items_5/{item_id}")
async def read_item_5(item_id: int):
    """
    处理 GET 请求，路径参数为 item_id。
    如果 item_id 为 3，则引发 HTTPException 异常，返回状态码 418 和自定义错误信息。
    否则，返回包含 item_id 的 JSON 响应。
    """
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}


#  todo FastAPI HTTPException vs Starlette HTTPException

"""
FastAPI HTTPException vs Starlette HTTPException

1. 定义和继承关系:
   - FastAPI 的 HTTPException:
     - 定义: `from fastapi import HTTPException`
     - 继承自: `starlette.exceptions.HTTPException`
     - 额外功能: 可以在响应中添加响应头。
     - 用途: 通常用于在路径操作中显式地抛出 HTTP 错误，并且可以附加自定义的响应头。

   - Starlette 的 HTTPException:
     - 定义: `from starlette.exceptions import HTTPException`
     - 继承自: `Exception`
     - 用途: 用于 Starlette 内部以及 FastAPI 的内部代码、扩展或插件中触发 HTTP 错误。

2. 用途和使用场景:
   - FastAPI 的 HTTPException:
     - 主要用于在 FastAPI 路径操作中显式抛出 HTTP 错误。
     - 例如，当某个条件不满足时，抛出 HTTP 400 错误:
       ```python
       @app.get("/items/{item_id}")
       async def read_item(item_id: int):
           if item_id == 3:
               raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
           return {"item_id": item_id}
       ```

   - Starlette 的 HTTPException:
     - 主要用于覆盖默认的 HTTP 异常处理器。
     - 例如，当需要自定义 HTTP 错误响应时:
       ```python
       @app.exception_handler(StarletteHTTPException)
       async def custom_http_exception_handler(request, exc):
           return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
       ```

3. 处理器注册:
   - 在 FastAPI 中，注册异常处理器时，应该注册到来自 Starlette 的 HTTPException。
     - 这样可以确保 Starlette 的内部代码、扩展或插件触发的 HTTPException 也能被处理。
     - 例如:
       ```python
       @app.exception_handler(StarletteHTTPException)
       async def custom_http_exception_handler(request, exc):
           return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
       ```

4. 响应头支持:
   - FastAPI 的 HTTPException:
     - 支持在响应中添加自定义响应头。
     - 例如:
       ```python
       raise HTTPException(status_code=400, detail="Invalid request", headers={"X-Error": "There goes my error"})
       ```

   - Starlette 的 HTTPException:
     - 不直接支持在响应中添加自定义响应头。

总结:
- 在路径操作中显式抛出 HTTP 错误时，使用 FastAPI 的 HTTPException。
- 当需要覆盖默认的 HTTP 异常处理器时，使用 Starlette 的 HTTPException。
- FastAPI 的 HTTPException 提供了额外的功能，可以在响应中添加自定义响应头。
"""
